﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using Borg.Properties;

namespace Borg
{
    [Slot("Startup", Multiple = true, OnPlugged = "StartupPlugged")]
    [Extension("Runtime", Singleton = true)]
    [Slot("Discovery", Multiple = true, OnPlugged = "DiscoveryPlugged", OnUnplugged = "DiscoveryUnplugged")]
    public class ClientRuntime : RuntimeBase
    {
        private Application _app;
        private readonly List<FileInfo> _discoveryList;
        private Dispatcher _dispatcher;
        private readonly IList<LogEventArgs> _initialLogs;
        private static bool _isConsloeVisible = true;
        private readonly object _lockObj;
        private readonly StreamWriter _logStreamWriter;
        private readonly Dictionary<ExtensionInfo, Thread> _threadExtensions;

        public ClientRuntime(RuntimeConfiguration configuration)
            : base(configuration)
        {
            _threadExtensions = new Dictionary<ExtensionInfo, Thread>();
            _lockObj = new object();

            UnhandledExceptionEventHandler handler =
                (sender, e) =>
                OnLogEvent(
                    this,
                    new LogEventArgs(
                        MessageType.Error,
                        VerbosityLevel.Minimal,
                        string.Format(
                            "Unhandled exception in {0}\n{1}",
                            AppDomain.CurrentDomain.FriendlyName,
                            e.ExceptionObject)));

            AppDomain.CurrentDomain.UnhandledException += handler;
            _discoveryList = configuration.DiscoveryList;
            _initialLogs = configuration.InitialLogs;

            try
            {
                if (configuration.LogfilePath == string.Empty)
                    return;

                _logStreamWriter = new StreamWriter(configuration.LogfilePath);

                _logStreamWriter.WriteLine(Runtime.VersionInfo());
                _logStreamWriter.WriteLine("Startup: " + startTime);
                _logStreamWriter.WriteLine("--------------------------------------------------------");
                _logStreamWriter.Flush();

                Runtime.LogEvent += LogInFile;
            }
            catch (Exception exception)
            {
                _initialLogs.Add(
                    new LogEventArgs(
                        MessageType.Error,
                        VerbosityLevel.Normal,
                        Messages.ErrorOnCreatingLogfile,
                        new object[]
                            {
                                configuration.LogfilePath,
                                string.Format(
                                    "\n  {0}",
                                    exception.Message)
                            }));
            }
        }

        private static void AbortThread(Thread t)
        {
            t.Abort();
        }

        internal static void AddFramworkComponents(IList<FileInfo> discoveryList)
        {
            var contractsFlag = false;
            var frameworkFlag = false;

            for (var i = discoveryList.Count - 1; i >= 0; i--)
            {
                if (discoveryList[i].Name.Equals("Borg.Contracts.dll", StringComparison.OrdinalIgnoreCase))
                {
                    MoveFile(discoveryList, i, 0);
                    contractsFlag = true;
                }
                else if (discoveryList[i].Name.Equals("Borg.Framework.dll", StringComparison.OrdinalIgnoreCase))
                {
                    MoveFile(discoveryList, i, contractsFlag ? 1 : 0);
                    frameworkFlag = true;
                }

                if (contractsFlag && frameworkFlag)
                    break;
            }

            if (contractsFlag && frameworkFlag) return;

            FileInfo current;
            FileInfo[] files;

            int len;

            var list = new List<DirectoryInfo>();

            using (var enumerator = discoveryList.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    current = enumerator.Current;

                    if (!list.Contains(current.Directory))
                        list.Add(current.Directory);
                }
            }

            if (!contractsFlag)
            {
                foreach (var dirInfo in list)
                {
                    files = dirInfo.GetFiles("Borg.Contracts.dll", SearchOption.AllDirectories);
                    len = 0;

                    while (len < files.Length)
                    {
                        current = files[len];
                        discoveryList.Insert(0, current);
                        contractsFlag = true;
                        break;
                    }

                    if (contractsFlag)
                        break;
                }
            }

            if (frameworkFlag)
                return;

            foreach (var dirInfo in list)
            {
                files = dirInfo.GetFiles("Borg.Framework.dll", SearchOption.AllDirectories);
                len = 0;

                while (len < files.Length)
                {
                    current = files[len];

                    discoveryList.Insert(contractsFlag ? 1 : 0, current);
                    frameworkFlag = true;
                    break;
                }

                if (frameworkFlag)
                    break;
            }
        }

        internal static void AddFramworkComponentsDelete(IList<FileInfo> discoveryList)
        {
            var contractsFlag = (discoveryList.Count > 0) &&
                discoveryList[0].Name.Equals("Borg.Contracts.dll", StringComparison.OrdinalIgnoreCase);

            var frameworkFlag = (discoveryList.Count > 1) &&
                discoveryList[1].Name.Equals("Borg.Framework.dll", StringComparison.OrdinalIgnoreCase);

            if (contractsFlag && frameworkFlag)
                return;

            FileInfo current;
            FileInfo[] files;

            int len;
            var list = new List<DirectoryInfo>();

            using (var enumerator = discoveryList.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    current = enumerator.Current;

                    if (!list.Contains(current.Directory))
                        list.Add(current.Directory);
                }
            }

            if (!contractsFlag)
            {
                foreach (var dirInfo in list)
                {
                    files = dirInfo.GetFiles("Borg.Contracts.dll", SearchOption.AllDirectories);
                    len = 0;

                    while (len < files.Length)
                    {
                        current = files[len];
                        discoveryList.Insert(0, current);
                        contractsFlag = true;
                        break;
                    }

                    if (contractsFlag)
                        break;
                }
            }

            frameworkFlag = (discoveryList.Count > 1) &&
                discoveryList[1].Name.Equals("Borg.Framework.dll", StringComparison.OrdinalIgnoreCase);

            if (contractsFlag && frameworkFlag)
                return;

            foreach (var dirInfo in list)
            {
                files = dirInfo.GetFiles("Borg.Framework.dll", SearchOption.AllDirectories);
                len = 0;

                while (len < files.Length)
                {
                    current = files[len];
                    discoveryList.Insert(contractsFlag ? 1 : 0, current);
                    frameworkFlag = true;
                    break;
                }

                if (frameworkFlag)
                    break;
            }
        }

        public static ClientRuntime Create(string[] discoveryPaths, string[] baseDirs,
            VerbosityLevel verbosity, Dictionary<string, string> args, string logfile, bool createAppDomain)
        {
            DirectoryInfo dirInfo;
            ClientRuntime runtime;

            if (Runtime.IsRunning)
                return (ClientRuntime)Runtime.Instance;

            var list = new List<LogEventArgs>();
            var discoveryList = new List<FileInfo>();

            if (discoveryPaths != null && discoveryPaths.Length > 0)
            {
                foreach (string str in discoveryPaths)
                {
                    if (File.Exists(str))
                        discoveryList.Add(new FileInfo(str));
                    else if (Directory.Exists(str))
                    {
                        var info = new DirectoryInfo(str);
                        discoveryList.AddRange(info.GetFiles("*.dll"));
                    }
                    else
                        list.Add(
                            new LogEventArgs(
                                MessageType.Warning, VerbosityLevel.Normal,
                                Messages.DiscoveryFileOrDirectoryNotFound, new object[] { str }));
                }
            }
            else
            {
                dirInfo = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
                discoveryList.AddRange(dirInfo.GetFiles("*.dll"));
            }

            AddFramworkComponents(discoveryList);
            var baseDirList = new List<string>();

            if (baseDirs != null && baseDirs.Length > 0)
            {
                foreach (var dir in baseDirs)
                {
                    if (Directory.Exists(dir))
                    {
                        dirInfo = new DirectoryInfo(dir);

                        if (!baseDirList.Contains(dirInfo.FullName))
                            baseDirList.Add(dirInfo.FullName);
                    }
                    else
                        list.Add(
                            new LogEventArgs(
                                MessageType.Warning,
                                VerbosityLevel.Normal,
                                Messages.ErrorOnAddingBaseDir,
                                new object[] { dir, string.Format("\n  {0}", Messages.DirectoryNotFound) }));
                }
            }
            else
                baseDirList.Add(AppDomain.CurrentDomain.BaseDirectory);

            var configuration = new RuntimeConfiguration
            {
                RootDirectory = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory),
                DiscoveryList = discoveryList,
                BaseDirectories = baseDirList,
                VerbosityLevel = verbosity,
                Arguments = args,
                LogfilePath = logfile,
                InitialLogs = list
            };

            if (createAppDomain)
                runtime = (ClientRuntime)GetInitialAppDomain(
                    discoveryList,
                    baseDirList).CreateInstance(
                        typeof(ClientRuntime).Assembly.FullName,
                        typeof(ClientRuntime).FullName,
                        false,
                        BindingFlags.Default,
                        null,
                        new object[] { configuration }, null, null).Unwrap();
            else
            {
                SetupAppDomain(GetAssemblyPaths(discoveryList, baseDirList));
                runtime = new ClientRuntime(configuration);
            }

            return runtime;
        }

        private static AppDomain CreateAppDomain(string friendlyName)
        {
            return AppDomain.CreateDomain(
                friendlyName,
                AppDomain.CurrentDomain.Evidence,
                AppDomain.CurrentDomain.SetupInformation);
        }

        private void CreateApplication()
        {
            _app = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown };
            _app.Startup += InAppInit;
            _app.Run();
        }

        public void DiscoveryPlugged(object sender, PlugEventArgs args)
        {
            var ext = (IDiscovery)args.Extension;
            ext.DiscoveryEvent += OnDiscoverEvent;
            ext.StartDiscovery(CreateTypeBuilder());
        }

        public void DiscoveryUnplugged(object sender, PlugEventArgs args)
        {
            ((IDiscovery)args.Extension).DiscoveryEvent -= OnDiscoverEvent;
        }

        internal void FinalizeInitialDiscovery()
        {
            if (!isFinalizing)
            {
                if (extension != null &&
                    (extension.SlotInfos[0].PluggedPlugInfos.Count > 0 ||
                     extension.SlotInfos[1].PluggedPlugInfos.Count > 0))
                    return;

                OnLogEvent(
                    this,
                    new LogEventArgs(
                        MessageType.Error,
                        VerbosityLevel.Minimal,
                        Messages.NoExtensionPlugged));

                if (Runtime.VerbosityLevel != VerbosityLevel.Quiet)
                    Console.ReadLine();
            }

            _app.Shutdown();
        }

        [DllImport("user32.dll")]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

        private static DirectoryInfo[] GetAssemblyPaths(IEnumerable<FileInfo> discoveryList, IEnumerable<string> baseDirList)
        {
            var set = new HashSet<string>();

            foreach (FileInfo info in discoveryList)
                set.Add(info.DirectoryName);

            foreach (var item in baseDirList.Select(dir => dir.EndsWith(@"\")
                ? dir.Substring(0, dir.Length - 1)
                : dir))
            {
                set.Add(item);
                var dirInfo = new DirectoryInfo(item);

                foreach (var directory in dirInfo.GetDirectories("*", SearchOption.AllDirectories))
                    set.Add(directory.FullName);
            }

            return (from path in set select new DirectoryInfo(path)).ToArray<DirectoryInfo>();
        }

        private static AppDomain GetInitialAppDomain(IEnumerable<FileInfo> discoveryList, ICollection<string> baseDirList)
        {
            var setup = new AppDomainSetup();
            var builder = new StringBuilder();

            foreach (var fileInfo in discoveryList.Where(fileInfo => !baseDirList.Contains(fileInfo.DirectoryName)))
                builder.AppendFormat("{0};", fileInfo.DirectoryName);

            foreach (var dir in baseDirList)
            {
                builder.AppendFormat("{0};", dir);
                var directory = new DirectoryInfo(dir);

                foreach (var dirInfo in directory.GetDirectories("*", SearchOption.AllDirectories).
                    Where(dirInfo => !baseDirList.Contains(dirInfo.FullName)))
                    builder.AppendFormat("{0};", dirInfo.FullName);
            }

            setup.PrivateBinPath = builder.ToString();
            setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

            setup.ShadowCopyDirectories = builder.ToString();
            setup.ShadowCopyFiles = true.ToString();

            return AppDomain.CreateDomain(string.Format("{0}.{1}", "default", "default"),
                AppDomain.CurrentDomain.Evidence, setup);
        }

        private void InAppInit(object sender, StartupEventArgs startupEventArgs)
        {
            Exception exception;
            List<ContractInfo> contractInfos;
            List<PluginInfo> pluginInfos;

            _dispatcher = new UserControl().Dispatcher;

            foreach (var logEventArgs in _initialLogs)
                OnLogEvent(this, logEventArgs);

            _initialLogs.Clear();
            var builder = CreateTypeBuilder();

            var analyzer = new AssemblyAnalyzer(OnLogEvent);

            try
            {
                ContractInfo[] ci;
                PluginInfo[] pi;

                analyzer.AnalyzeFile("Borg.Initializer.dll", builder, out ci, out pi);

                foreach (var contract in ci)
                    AddInitialContract(contract);

                foreach (var plugin in pi)
                    AddInitialPlugin(plugin);

                analyzer.AnalyzeFile("Borg.dll", builder, out ci, out pi);

                foreach (var contract in ci)
                    AddInitialContract(contract);

                foreach (var plugin in pi)
                    AddInitialPlugin(plugin);

                CreateRuntimeExtension(Repository.ExtensionTypeInfos["Runtime"], this);
            }
            catch (Exception ex)
            {
                exception = ex;

                OnLogEvent(
                    this,
                    new LogEventArgs(
                        MessageType.Error,
                        VerbosityLevel.Normal,
                        exception.Message));
            }

            Initializer.DiscoverInitialAssemblies(
                builder,
                _discoveryList,
                OnLogEvent,
                out contractInfos,
                out pluginInfos);

            foreach (var ci in contractInfos)
            {
                try
                {
                    AddInitialContract(ci);
                }
                catch (Exception ex)
                {
                    exception = ex;

                    OnLogEvent(
                        this,
                        new LogEventArgs(
                            MessageType.Error,
                            exception.Message));
                }
            }

            extension.SlotInfos["Startup"].Open();
            extension.SlotInfos["Discovery"].Open();

            foreach (var pi in pluginInfos)
            {
                try
                {
                    AddInitialPlugin(pi);
                }
                catch (Exception ex)
                {
                    exception = ex;

                    OnLogEvent(
                        this,
                        new LogEventArgs(
                            MessageType.Error,
                            exception.Message));
                }
            }

            foreach (var pi in repository.PluginInfos)
            {
                try
                {
                    RegisterInitialPlugin(pi);
                }
                catch (Exception ex)
                {
                    exception = ex;

                    OnLogEvent(
                        this,
                        new LogEventArgs(
                            MessageType.Error,
                            exception.Message));
                }
            }

            lock (_lockObj)
            {
                OnStartupCompleted(this, EventArgs.Empty);
                Monitor.PulseAll(_lockObj);
            }
        }

        protected override object Invoke(Delegate method, params object[] args)
        {
            try
            {
                return Invoke0(method, args);
            }
            catch (Exception innerException)
            {
                while (innerException.InnerException != null)
                    innerException = innerException.InnerException;

                OnLogEvent(
                    this,
                    new LogEventArgs(
                        MessageType.Error,
                        VerbosityLevel.Normal,
                        Messages.UnhandledException,
                        new object[] { string.Format(":\n  {0}", innerException) }));
            }

            return null;
        }

        private object Invoke0(Delegate method, object[] args)
        {
            return _dispatcher.CheckAccess()
                ? method.DynamicInvoke(args)
                : _dispatcher.Invoke(method, args);
        }

        protected override bool IsRuntimeThread()
        {
            return _dispatcher.CheckAccess();
        }

        internal void LogInFile(object sender, LogEventArgs args)
        {
            if (args.MessageType != MessageType.Info)
                _logStreamWriter.WriteLine(
                    args.MessageType == MessageType.Error
                        ? string.Format("\n--ERROR:\n{0}\n", args.Message)
                        : string.Format("\n--WARNING:\n{0}\n", args.Message));
            else
                _logStreamWriter.WriteLine(args.Message);

            _logStreamWriter.Flush();
        }

        private static void MoveFile(IList<FileInfo> list, int from, int to)
        {
            if (@from == to)
                return;

            var file = list[@from];

            list.RemoveAt(@from);
            list.Insert(to, file);
        }

        protected override void OnLogEvent(object sender, LogEventArgs args)
        {
            if (args.Verbosity <= verbosityLevel)
            {
                var foregroundColor = Console.ForegroundColor;
                var element = sender as ExtensionInfo;

                if (element != null)
                {
                    Console.ForegroundColor = ConsoleColor.Green;
                    Console.Write("[{0}:{1}]: ", element.Id, element.Name);
                    Console.ForegroundColor = foregroundColor;
                }

                if (args.MessageType != MessageType.Info)
                    Console.ForegroundColor =
                        args.MessageType == MessageType.Error
                            ? ConsoleColor.Red
                            : ConsoleColor.Yellow;

                Console.WriteLine(args.Message);
                Console.ForegroundColor = foregroundColor;
            }

            base.OnLogEvent(sender, args);
        }

        private void Run()
        {
            _app = Application.Current;
            InAppInit(null, null);
        }

        internal static void SetConsoleWindowVisibility(bool visible)
        {
            var hWnd = FindWindow(null, Console.Title);

            if (hWnd == IntPtr.Zero)
                return;

            ShowWindow(hWnd, !visible ? 0 : 1);

            _isConsloeVisible = visible;
        }

        private static void SetupAppDomain(IEnumerable<DirectoryInfo> assemblyPaths)
        {
            foreach (var directory in assemblyPaths)
                AppDomain.CurrentDomain.AppendPrivatePath(directory.FullName);

            AppDomain.CurrentDomain.SetShadowCopyFiles();
        }

        [DllImport("user32.dll")]
        private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        protected override void Shutdown(Assembly initiator)
        {
            base.Shutdown(initiator);

            if (_logStreamWriter != null)
            {
                _logStreamWriter.WriteLine("--------------------------------------------------------");
                _logStreamWriter.WriteLine("Shutdown: " + DateTime.Now);
                _logStreamWriter.WriteLine("Uptime: " + UpTime);
                _logStreamWriter.Close();
            }

            _app.Shutdown();
        }

        public void Start()
        {
            var thread = new Thread(CreateApplication);
            thread.SetApartmentState(ApartmentState.STA);

            lock (_lockObj)
            {
                thread.Start();
                Monitor.Wait(_lockObj);
            }
        }

        public void Start1()
        {
            var thread = new Thread(Run);
            thread.SetApartmentState(ApartmentState.STA);

            lock (_lockObj)
            {
                thread.Start();
                Monitor.Wait(_lockObj);
            }
        }

        public void StartupPlugged(object sender, PlugEventArgs args)
        {
            if ((bool)args.GetParamValue("ExecuteInMainThread"))
                ((IStartup)args.Extension).Run();
            else
            {
                var ext = (IStartup)args.Extension;
                var thread = new Thread(ext.Run);

                _threadExtensions.Add(args.PlugInfo.ExtensionInfo, thread);
                args.PlugInfo.ExtensionInfo.Released += ThreadExtensionReleased;

                thread.SetApartmentState(ApartmentState.STA);
                thread.Start();
            }
        }

        private void ThreadExtensionReleased(object sender, ExtensionEventArgs args)
        {
            if (!_threadExtensions.ContainsKey(args.ExtensionInfo))
                return;

            var thread = _threadExtensions[args.ExtensionInfo];
            AbortThread(thread);
            _threadExtensions.Remove(args.ExtensionInfo);
        }

        protected override bool ConsoleWindow
        {
            get { return _isConsloeVisible; }
            set
            {
                if (value != _isConsloeVisible)
                    SetConsoleWindowVisibility(value);
            }
        }

        private delegate void NoParameterHandler();
    }
}

